/*  -*- mode: c++; coding: utf-8; c-file-style: "stroustrup"; -*-

    Copyright 2008 Asier Aguirre <asier.aguirre@gmail.com>
    This file is part of Intromit.

    Intromit is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Intromit is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Intromit.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pymetadata_container.hh"
#include "pymetadata_global.hh"
#include "pymetadata.hh"
#include "gendata_common.hh"
#include "util.hh"

// DEFINITIONS
#define INC_GLOBAL_VAR       32

// PRIVATE CLASS
CLASS_INIT(python_container); // PYTHON_CONTAINER

// static member variable (TODO: memory leak)
PyTypeObject *python_container::python_container_type=NULL;

python_container::python_container()
{
    CLASS_CTOR(python_container);
    ob_refcnt=1;
    ob_type=NULL;
    new_class=NULL;
    dict=NULL;

    // global variables
    util::ctor_vector(gvar, n_gvar, max_gvar);
    pytype=NULL;
    pytype_data=NULL;
    pytype_doc=NULL;
    pytype_prepared=false;

    // functions
    n_functions=max_functions=0;
}

python_container::~python_container()
{
    CLASS_DTOR(python_container);
    if(dict) {Py_DECREF(dict);}
    if(ob_refcnt) ERROR("reference count (=%d) should be zero", ob_refcnt);

    // destroy variables
    for(int i=0; i<n_gvar; i++)
	if(gvar[i].pyobj) gvar[i].pyobj->python_decref();

    util::dtor_vector(gvar, n_gvar, max_gvar);
    if(pytype) delete pytype;
    if(pytype_data) free(pytype_data);
}

PyTypeObject *python_container::get_python_container_type()
{
    if(python_container_type) return python_container_type;

    // create the python container type
    python_container_type=(PyTypeObject *)malloc(sizeof(PyTypeObject));

    memset(python_container_type, 0, sizeof(PyTypeObject));
    PyTypeObject dummy={PyObject_HEAD_INIT((PyTypeObject*)NULL)};
    memcpy(python_container_type, &dummy, sizeof(PyObject));

    python_container_type->tp_name=const_cast<char*>("pyintromit.container");
    python_container_type->tp_basicsize=sizeof(python_container);
    python_container_type->tp_flags=Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
	Py_TPFLAGS_CHECKTYPES/*allow multitype operatos*/;
    python_container_type->tp_doc=NULL;

    python_container_type->tp_new=pynew;
    python_container_type->tp_init=pyinit;
    python_container_type->tp_dealloc=pydealloc;
    python_container_type->tp_call=pycall;
    python_container_type->tp_getattro=pygetattr;
    python_container_type->tp_setattro=pysetattr;

    // get the type ready
    if(PyType_Ready(python_container_type)<0)
	ERROR_R0("PyType_Ready: class %s", python_container_type->tp_name);

    return python_container_type;
}

PyObject *python_container::pynew(PyTypeObject *type_, PyObject *args, PyObject *kwds)
{
    PyTypeObject *new_class=NULL;
    if(args && !PyArg_ParseTuple(args, (char*)"O", &new_class))
    {
	if(!PyErr_Occurred()) PyErr_SetString(PyExc_TypeError, "New class type required");
	return NULL;
    }

    PyObject *obj=(PyObject*)new python_container;
    python_container *container=(python_container*)obj;
    obj->ob_type=(PyTypeObject*)type_;
    container->new_class=new_class;
    return obj;
}

PyObject *python_container::new_container(PyTypeObject *type)
{
    PyObject *container;
    PyObject *args=NULL;
    if(type)
    {
	args=PyTuple_New(1);
	Py_INCREF(type);
	PyTuple_SET_ITEM(args, 0, (PyObject*)type);
    }
    if(!(container=pynew(get_python_container_type(), args, NULL/*kwds*/)))
	ERROR("Cannot generate container object");
    if(args) {Py_DECREF(args);}
    return container;
}

int python_container::pyinit(PyObject *self, PyObject *args, PyObject *kwds)
{
    return 0;
}

void python_container::pydealloc(PyObject *self)
{
    delete ((python_container*)self);
}

PyObject *python_container::pycall(PyObject *self, PyObject *args, PyObject *kwds)
{
    python_container *cont=(python_container*)self;
    if(!cont->new_class)
    {
	PyErr_SetString(PyExc_TypeError, "This container represents a pure C++ namespace and is not callable");
	return NULL;
    }
    return python_type_metadata::pynew(cont->new_class, args, kwds);
}

PyObject *python_container::pygetattr(PyObject *self, PyObject *pyname)
{
    python_container *cont=(python_container*)self;
    PyTypeObject *type=cont->new_class;
    PyObject *value;
    char *name=PyString_AsString(pyname);
    int i_gvar;

    if(!strcmp(name, "__dict__"))
    {
	// first copy own dict
	PyObject *dict;
	if(cont->dict) dict=PyDict_Copy(cont->dict);
	else dict=PyDict_New();

	// then add new class attributes
	for(int i=0; type && type->tp_getset[i].name; i++)
	    PyDict_SetItemString(dict, type->tp_getset[i].name, PyLong_FromLong(i));
	for(int i=0; type && type->tp_methods[i].ml_name; i++)
	    PyDict_SetItemString(dict, type->tp_methods[i].ml_name, PyLong_FromLong(i));

	// finally add global variables
	for(int i=0; i<cont->n_gvar; i++)
	{
	    PyObject *item;
	    if((item=cont->get_global_variable(i)))
		PyDict_SetItemString(dict, cont->gvar[i].name, item);
	    else
		PyErr_Clear();
	}

	return dict;
    }
    else if(cont->new_class && PyObject_HasAttr((PyObject*)cont->new_class, pyname))
	return PyObject_GetAttr((PyObject*)cont->new_class, pyname);
    else if((i_gvar=cont->search_global_variable(name))>=0)
	return cont->get_global_variable(i_gvar);
    else if(cont->dict && (value=PyDict_GetItem(cont->dict, pyname)))
    {
	// this is a trick: instead of returning functions container, return pytype object
	// so that pytype methods could be executed
	if(value->ob_type==python_container_type && !strcmp(name, "functions"))
	{
	    python_container *func=(python_container *)value;
	    if(func->pytype)
	    {
		func->prepare_pytype();
		return (PyObject*)python_metadata::new_python_metadata(malloc(sizeof(int)), 0/*indir*/, NULL/*container*/, func->pytype, NULL/*ctor_args*/);
	    }
	}

	Py_INCREF(value);
	return value;
    }
    else if(cont->new_class)
	return PyObject_GetAttr((PyObject*)cont->new_class, pyname);

    pymdg.clear_error_string();
    pymdg.add_error_string("Container has no %s attribute", name);
    PyErr_SetString(PyExc_AttributeError, pymdg.get_error_string());
    return NULL;
}

int python_container::pysetattr(PyObject *self, PyObject *pyname, PyObject *value)
{
    python_container *cont=(python_container*)self;
    char *name=PyString_AsString(pyname);

    // global variables
    int i_gvar;
    if((i_gvar=cont->search_global_variable(name))>=0)
	return cont->set_global_variable(i_gvar, value);

    if(!cont->dict) cont->dict=PyDict_New();
    if(PyDict_GetItem(cont->dict, pyname))
    {
	// cannot set an added value
	PyErr_SetString(PyExc_AttributeError, "Trying to modify constant value");
	return -1;
    }

    // value refcnt is increased
    return PyDict_SetItem(cont->dict, pyname, value);
}

PyObject *python_container::route(PyObject *module, char *namespc, char *name, int completing_allowed, int name_is_container)
{
    // namespc
    while(namespc)
    {
	char *restore_pos;
	int restore_char;
	char *name_next=split_namespace(namespc, &restore_pos, &restore_char);
	if(!PyObject_HasAttrString(module, namespc))
	{
	    if(!completing_allowed)
	    {
		if(name_next) *restore_pos=restore_char;
		return NULL;
	    }

	    // create component by means of container
	    PyObject *container;
	    if(!(container=new_container())) return NULL;
	    if(PyObject_SetAttrString(module, namespc, container)) ERROR_R0("PyObject_SetAttrString");
	    Py_DECREF(container);
	}

	module=PyObject_GetAttrString(module, namespc);
	Py_DECREF(module); // from new reference to borrow reference
	if(name_next) *restore_pos=restore_char;
	namespc=name_next;
    }

    // name
    if(!name) return module;

    if(!PyObject_HasAttrString(module, name))
    {
	if(name_is_container)
	{
	    // create name as a container
	    PyObject *container;
	    if(!(container=new_container())) return NULL;
	    if(PyObject_SetAttrString(module, name, container)) ERROR_R0("PyObject_SetAttrString");
	    Py_DECREF(container);
	    return container;
	}
	return NULL;
    }
    module=PyObject_GetAttrString(module, name);
    Py_DECREF(module); // from new reference to borrow reference
    return module;
}

char *python_container::split_namespace(char *name, char **restore_pos, int *restore_char)
{
    char *dot=strchr(name, '.');
    char *colons=strstr(name, "::");
    char *pos=NULL;
    int n_pos;

    if(dot && colons)
    {
	if(dot<colons) {pos=dot; n_pos=1;}
	else {pos=colons; n_pos=2;}
    }
    else if(dot) {pos=dot; n_pos=1;}
    else if(colons) {pos=colons; n_pos=2;}
    else return NULL;

    if(restore_char) *restore_char=*pos;
    if(restore_pos) *restore_pos=pos;
    *pos=0;
    return pos+n_pos;
}

int python_container::add_global_variable(char *name, int *data, void *addr)
{
    if(addr)
    {
	int i=util::inc_vector(gvar, n_gvar, max_gvar, INC_GLOBAL_VAR);
	gvar[i].name=name;
	gvar[i].data=data;
	gvar[i].pyobj=NULL;
	data[gendata_get_position(data, GENDATA_GENDATA_RESERVE)]=(int)addr;
	return i;
    }
    return -1;
}

int python_container::search_global_variable(char *name)
{
    // TODO: sorted search here, please
    for(int i=0; i<n_gvar; i++) if(!strcmp(name, gvar[i].name)) return i;
    return -1;
}

PyObject *python_container::get_global_variable(int i)
{
    void *type_pointer;
    int *data, type_metadata=-1;
    md_c_to_python_func func;

    if(!(type_pointer=(void*)gvar[i].data[gendata_get_position(gvar[i].data, GENDATA_GENDATA_RESERVE)]) ||
       !(data=gendata_resolve_pos_type(gvar[i].data)) ||
       (type_metadata=*data)<0 ||
       (!(func=pymdg.get_func_c_to_python(type_metadata)) && type_metadata!=GENDATA_CLASS))
    {
	pymdg.clear_error_string();
	pymdg.add_error_string("internal: check get function for type %s",
			       type_metadata>=0?gendata_type_strings[type_metadata]:"unknown");
	PyErr_SetString(PyExc_TypeError, pymdg.get_error_string());
	return NULL;
    }

    // special object cases
    /* EXPLANATION:
         In case of global variables of type class or object pointer, a
	 trick is done in order to reuse member object and member object
	 pointer functions.
	 A new python object is created (gvar[i].pyobj) of a brand new
	 type (pytype). This C/python object holds a pointer to the global
	 object. Finally, the real python object is created normally and
	 it is given type P4.
    */
    if(type_metadata==GENDATA_CLASS || type_metadata==GENDATA_OBJECT_POINTER)
    {
	if(type_metadata==GENDATA_CLASS) func=pymdg.get_func_c_to_python(GENDATA_OBJECT_POINTER);

	// check/create type for python containers
	create_pytype();

	// check/create container
	void **obj;
	if(!gvar[i].pyobj)
	{
	    gvar[i].pyobj=python_metadata::new_python_metadata(malloc(sizeof(int)), 0/*indir*/, NULL/*container*/, pytype, NULL/*ctor_args*/);
	    obj=(void**)gvar[i].pyobj->get_obj();
	    *obj=type_pointer;
	}
	else
	    obj=(void**)gvar[i].pyobj->get_obj();

	// update refernece in case of global object pointer
	if(type_metadata==GENDATA_OBJECT_POINTER) *obj=*(void**)type_pointer;
	type_pointer=obj;
    }
    return func(type_pointer, data);
}

void python_container::create_pytype()
{
    if(!pytype)
    {
	if(!pytype_doc) ERROR("!pytype_doc");
	pytype_data=(int*)calloc(gendata_type_size(GENDATA_CLASS), sizeof(int));
	pytype_data[0]=GENDATA_CLASS;
	pytype_data[gendata_get_position(pytype_data, GENDATA_NAME)]=strlen(pytype_doc); // empty string
	pytype_data[gendata_get_position(pytype_data, GENDATA_DOC)]=-1;
	pytype_data[gendata_get_position(pytype_data, GENDATA_NAMESPACE)]=-1;
	pytype_data[gendata_get_position(pytype_data, GENDATA_DOC)]=-1;
	pytype_data[gendata_get_position(pytype_data, GENDATA_SIZE)]=4;
	pytype_data[gendata_get_position(pytype_data, GENDATA_GENDATA_VAR)]=0;
	pytype=new python_type_metadata(const_cast<char*>("_tmp_"), pytype_data, pytype_doc, NULL);

	// calculate n_functions and max_functions
	while(pytype->get_method(n_functions)->ml_name) n_functions++;
	max_functions=n_functions;
    }
}

void python_container::prepare_pytype()
{
    if(pytype && !pytype_prepared)
    {
	pytype_prepared=true;
	pytype->ready();
	pytype->update_methods();
    }
}

int python_container::set_global_variable(int i, PyObject *value)
{
    void *type_pointer;
    int *data, type_metadata=-1;
    md_python_to_c_func func;

    if(!(type_pointer=(void*)gvar[i].data[gendata_get_position(gvar[i].data, GENDATA_GENDATA_RESERVE)]) ||
       !(data=gendata_resolve_pos_type(gvar[i].data)) ||
       (type_metadata=*data)<0 ||
       !(func=pymdg.get_func_python_to_c(type_metadata)))
    {
	pymdg.clear_error_string();
	pymdg.add_error_string("internal: check set function for type %s",
			       type_metadata>=0?gendata_type_strings[type_metadata]:"unknown");
	PyErr_SetString(PyExc_TypeError, pymdg.get_error_string());
	return -1;
    }
    return func(value, type_pointer, data);
}

int python_container::add_function(char *name, int *data, void *dl)
{
    create_pytype();

    int n_func2=n_functions;
    int max_func2=max_functions;
    int dev=pytype->add_method(data, dl, n_functions, max_functions, n_func2, max_func2);
    get_functions();

    return dev;
}

PyMethodDef *python_container::get_functions()
{
    create_pytype();

    // add terminator
    int n_func2=n_functions;
    int max_func2=max_functions;
    pytype->centinel_method(n_func2, max_func2);

    // add python object context to functions
    // NOTE: this is to reuse class method functions
    pytype->set_context_object_for_functions(pytype);

    return pytype->get_method(0);
}
